    ; 内核加载器
    ; @author: Milon
    ; @date: 2024-04-20

%include "./common/global_defs.h"

section loader
    marker              dd  'Miln'
    length              dd  loader_end
    entry               dd  start

    msg0                db "Milon's x64 core.",0x0d,0x0a

    arch0               db "x64 available(64-bit processor installed).",0x0d,0x0a
    arch1               db "x64 not available(64-bit processor not installed).",0x0d,0x0a

    brand_msg           db "Processor:"
    brand   times 48    db 0
                        db  0x0d,0x0a

    cpu_addr            db "Physical address size:"
    paddr   times  3    db ' '
                        db ", "
                        db "Linear address size:"
    laddr   times  3    db ' '
                        db 0x0d,0x0a

    protect             db "Protect mode has been entered to prepare for IA-32e mode.",0x0d,0x0a,0

    ia_32e              db "IA-32e mode(aka,long mode) is active.Specifically,compatibility mode.",0x0d,0x0a,0



no_ia_32e:
        
        mov ah, 0x03                ; 功能号0x03，读光标位置
        mov bh, 0                   ; 页号，文本模式可置0
        int 0x10                    ; dh:dl = 光标所在行:列

        mov ah, 0x13                ; 功能号0x13，显示字符串
        mov al, 0x01                ; 带光标跟随写屏
        mov bh, 0
        mov bl, 10000000B           ; 红底黑字
        mov cx, brand_msg - arch1   ; 字符串arch1长度
        mov bp, arch1               ; es:bp = 字符串所在逻辑段地址:段内偏移量
        int 0x10

        cli
        hlt


start:
        mov ah, 0x03                ; 功能号0x03，读光标位置
        mov bh, 0                   ; 页号，文本模式可置0
        int 0x10                    ; dh:dl = 光标所在行:列

        mov ah, 0x13                ; 功能号0x13，显示字符串
        mov al, 0x01                ; 带光标跟随写屏
        mov bh, 0
        mov bl, 0010_1111B          ; 显示属性
        mov cx, arch0 - msg0
        mov bp, msg0                ; es:bp = 字符串所在逻辑段地址:段内偏移量
        int 0x10

        
        mov eax, 0x80000000         ; 获取处理器最大支持的功能号
        cpuid
        cmp eax, 0x80000000
        jle no_ia_32e

        
        mov eax, 0x80000001         ; 功能号0x80000001返回扩展签名和特性标志位
        cpuid
        bt edx, 29                  ; edx的位29代表是否支持IA-32e
        jnc no_ia_32e

        mov ah, 0x03                ; 功能号0x03，读光标位置
        mov bh, 0                   ; 页号，文本模式可置0
        int 0x10                    ; dh:dl = 光标所在行:列

        mov ah, 0x13                ; 功能号0x13，显示字符串
        mov al, 1                   ; 带光标跟随写屏
        mov bh, 0
        mov bl, 00000111B           ; 显示属性
        mov cx, arch1 - arch0
        mov bp, arch0               ; es:bp = 字符串所在逻辑段地址:段内偏移量
        int 0x10

        mov eax, 0x80000000
        cpuid
        cmp eax, 0x80000004         ; 判断处理器是否支持0x80000002~0x80000004功能号
        jl .no_brand

        ; 以下获取CPU商标信息
        mov eax, 0x80000002
        cpuid
        mov [brand + 0x00], eax
        mov [brand + 0x04], ebx
        mov [brand + 0x08], ecx
        mov [brand + 0x0c], edx

        mov eax, 0x80000003
        cpuid
        mov [brand + 0x10], eax
        mov [brand + 0x14], ebx
        mov [brand + 0x18], ecx
        mov [brand + 0x1c], edx

        mov eax, 0x80000004
        cpuid
        mov [brand + 0x20], eax
        mov [brand + 0x24], ebx
        mov [brand + 0x28], ecx
        mov [brand + 0x2c], edx

        ; 以下打印CPU商标信息
        mov ah, 0x03                    ; 功能号0x03，读光标位置
        mov bh, 0                       ; 页号，文本模式可置0
        int 0x10                        ; dh:dl = 光标所在行:列
        mov ax, 0x1301                  ; 功能号0x13，显示字符串，带光标跟随写屏
        mov bh, 0
        mov bl, 0010_1111B              ; 显示属性
        mov cx, cpu_addr - brand_msg
        mov bp, brand_msg               ; es:bp = 字符串所在逻辑段地址:段内偏移量
        int 0x10

    .no_brand:
        push es
        
        mov bx, SDA_PHY_ADDR >> 4
        mov es, bx
        mov di, 0x18                    ; es:di指向系统数据区偏移量为18的地方（地址范围描述结构列表）
        mov word [es:0x16], 0           ; 地址范围描述结构的个数 初始化为0
        xor ebx, ebx                    ; ebx=持续标记，首次调用int 0x15时清零
    .mlookup:
        mov eax, 0xe820                 ; 使用BIOS的E820功能获取当前系统的物理内存布局
        mov ecx, 32                     ; 地址范围描述结构长度，以字节计
        mov edx, 'PAMS'                 ; 签名，必须是字符串'PAMS'的编码(Physical Address Mapping Structure)
        int 0x15
        add di, 32
        inc word [es:0x16]              ; 地址范围描述结构的个数+1
        or ebx, ebx                     ; 检查ebx是否为0，如果是则说明这是返回的最后一个地址范围数据
        jnz .mlookup

        pop es
        
        mov eax, 0x80000000
        cpuid
        cmp eax, 0x80000008             ; 判断CPU是否支持0x80000008功能号
        mov ax, 0x3024                  ; 默认地址尺寸 0x30=48位线性地址，0x24=36位物理地址
        jl .no_plsize
        mov eax, 0x80000008             ; 功能号0x80000008，获取物理地址宽度到al中，线性地址宽度到ah中
        cpuid

    .no_plsize:
        push ds
        mov cx, SDA_PHY_ADDR >> 4
        mov ds, cx
        mov [0x00], ax                  ; 保存线性、物理地址尺寸到系统数据区
        pop ds

        ; 以下分解物理地址尺寸各位
        push ax
        and ax, 0x00ff                  ; 清空ax高16位，保留低16位
        mov bl, 10
        mov si, 2                       ; 2代表字符串起始位置的偏移量
    .re_div0:
        div bl                          ; 除以10，余数在ah中
        add ah, '0'                     ; 余数+字符0的ASCII编码，得到余数的ASCII编码
        mov [paddr + si], ah
        dec si                          ; 个位放在最后，其它位依次往前递减
        and ax, 0x00ff
        jnz .re_div0                    ; 如果商不等于0，则继续分解，否则结束

        ; 分解线性地址尺寸
        pop ax
        shr ax, 8
        mov si, 2
    .re_div1:
        div bl
        add ah, '0'
        mov [laddr + si], ah
        dec si
        and ax, 0x00ff
        jnz .re_div1

        ; 以下显示物理、线性地址尺寸
        mov ah, 0x03                    ; 功能号0x03，读光标位置
        mov bh, 0                       ; 页号，文本模式可置0
        int 0x10
        mov ax, 0x1301                  ; 功能号0x13，显示字符串，带光标跟随写屏
        mov bh, 0
        mov bl, 00000111B               
        mov cx, protect - cpu_addr
        mov bp, cpu_addr                ; es:bp = 字符串所在逻辑段地址:段内偏移量
        int 0x10

        ; ================= 创建保护模式下的段描述符 =================

        mov ax, GDT_PHY_ADDR >> 4
        mov ds, ax

        ; #0 空描述符
        mov dword [0x00], 0
        mov dword [0x04], 0

        ; #1 代码段描述符
        mov dword [0x08], 0x0000ffff        ; 使用平坦模型，段基地址0，段界限0xffffffff
        mov dword [0x0c], 0x00cf9800        ; 粒度4K，D=1，L=0，P=1，DPL=00，S=1，X=1

        ; #2 数据段，栈段描述符
        mov dword [0x10], 0x0000ffff        ; 使用平坦模型，段基地址0，段界限0xffffffff
        mov dword [0x14], 0x00cf9200        ; 粒度4K，B=1，L=0，P=1，DPL=00，S=1，X=0，W=1

        ; #3 64位模式代码段描述符，L=1
        mov dword [0x18], 0x0000ffff        ; 64位强制使用平坦模型，不再检查段基地址和段界限
        mov dword [0x1c], 0x00af9800        ; D=0，L=1，P=1，DPL=00，S=1，X=1

        mov ax, SDA_PHY_ADDR >> 4
        mov ds, ax                          ; ds指向系统数据区
        mov word [0x02], 4 * 8 - 1          ; 向系统数据区写入GDT界限值
        mov dword [0x04], GDT_PHY_ADDR      ; 向系统数据区写入GDT物理地址
        lgdt [0x02]                         ; 加载GDT

        ; 开启A20地址线
        in al, 0x92
        or al, 0000_0010B
        out 0x92, al

        cli                                 ; 进入保护模式之前，先关闭中断

        ; CR0 PE位置1，开启保护模式
        mov eax, cr0
        or eax, 0x01
        mov cr0, eax

        jmp 0x08: dword LDR_PHY_ADDR + flush   ; 清空流水线并串行化处理器

        ; 以下代码均用32位编译
        [bits 32]

flush:
        mov eax, 0x10                           ; 段选择子RPL=00，TI=0，索引=10，选择2号数据段描述符
        mov ds, eax
        mov es, eax
        mov fs, eax
        mov gs, eax
        mov ss, eax
        mov esp, 0x7c00

        mov ebx, LDR_PHY_ADDR + protect         ; 进入保护模式后无法再使用BIOS中断，调用put_string例程打印字符串
        call put_string
        
        
        mov edi, CORE_PHY_ADDR                  ; 内核起始物理地址
        mov eax, COR_START_SECTOR               ; 内核所在起始逻辑扇区号
        mov ebx, edi
        call read_hard_disk_0                   ; 从硬盘读取内核第一个扇区

        mov eax, [edi]                          ; 内核起始物理地址头部4字节保存了内核程序长度
        xor edx, edx
        mov ecx, 512                            ; 内核长度 / 512 判断占用了几个扇区
        div ecx
        or edx, edx                             ; 余数是否为0？
        jnz .keep                               ; 不等于0则跳转
        dec eax                                 ; 等于0则剩余扇区数-1（刚刚已经读了一个扇区）
    .keep:
        or eax, eax                             ; 判断剩余扇区数是否为0
        jz .loaded                              ; 如果为0说明已经读取完毕，直接跳转

        ; 余数不等于0，则循环读取剩余扇区
        mov ecx, eax
        mov eax, COR_START_SECTOR + 1
    .reduce:
        call read_hard_disk_0
        inc eax
        loop .reduce

    .loaded:
        ; 回填内核加载地址 -> 内核头部
        mov dword [CORE_PHY_ADDR + 0x08], CORE_PHY_ADDR
        mov dword [CORE_PHY_ADDR + 0x0c], 0

        ; 以下准备四级分页
        ; 清空四级头表中的512个表项
        mov ebx, PML4_PHY_ADDR
        xor esi, esi
    .cls_pml4:
        mov dword [ebx + esi * 8], 0
        mov dword [ebx + esi * 8 + 4], 0
        inc esi
        cmp esi, 512
        jl .cls_pml4

        ; 四级头表最后一个表项指向自己
        mov dword [ebx + 511 * 8], PML4_PHY_ADDR | 3    ; US=0, RW=P=1
        mov dword [ebx + 511 * 8 + 4], 0

        ; 以下将低端2M内存物理地址和线性地址映射为相同的数值

        ; 安装四级头表第0个表项，指向页目录指针表
        mov dword [ebx], PDPT_PHY_ADDR | 3              ; US=0, RW=P=1
        mov dword [ebx + 4], 0                          

        ; 将页目录指针表所有表项清0
        xor esi, esi
        mov ebx, PDPT_PHY_ADDR
    .cls_pdpt:
        mov dword [ebx + esi * 8], 0
        mov dword [ebx + esi * 8 + 4], 0
        inc esi
        cmp esi, 512
        jl .cls_pdpt

        ; 页目录指针表第0号表项安装页目录表项
        mov dword [ebx], PDT_PHY_ADDR | 3               ; US=0, RW=P=1
        mov dword [ebx + 4], 0

        ; 将页目录表清0
        xor esi, esi
        mov ebx, PDT_PHY_ADDR
    .cls_pdt:
        mov dword [ebx + esi * 8], 0
        mov dword [ebx + esi * 8 + 4], 0
        inc esi
        cmp esi, 512
        jl .cls_pdt

        ; 在页目录表内创建指向2MB物理页的表项
        mov dword [ebx], 10000011B                      ; 位7=1代表指向2MB物理页，US=0, RW=P=1
        mov dword [ebx + 4], 0

        ; 将低端2MB内存映射到线性地址高端，所以在高端线性地址对应的四级头表项安装页目录指针表
        mov ebx, PML4_PHY_ADDR
        mov dword [ebx + 256 * 8], PDPT_PHY_ADDR | 3
        mov dword [ebx + 256 * 8 + 4], 0

        ; 在四级头表高一半预先创建剩余254个表项（避免多任务环境下，不同任务修改了四级头表项需要同步给其它所有任务）
        mov esi, 257
        mov edx, COR_PDPT_ADDR | 3                      ; US=0, RW=P=1
    .fill_pml4:
        mov dword [ebx + esi * 8], edx
        mov dword [ebx + esi * 8 + 4], 0
        inc esi
        add edx, 0x1000                                 ; +4KB，指向下一个页目录指针表
        cmp esi, 511
        jl .fill_pml4

        ; 将预分配的254个四级头表项指向的页目录指针表清0
        mov ebx, COR_PDPT_ADDR
    .cls_cor_pdpt:
        mov dword [ebx], 0
        add ebx, 4
        cmp ebx, COR_PDPT_ADDR + 254 * 0x1000
        jl .cls_cor_pdpt

        ; CR3指向四级头表（保护模式下32位）
        mov eax, PML4_PHY_ADDR                          ; PCD = PWT = 0
        mov cr3, eax

        mov eax, cr4
        bts eax, 5                                      ; CR4的位5是PAE位，将该位置1开启PAE
        mov cr4, eax

        ; 设置型号专属寄存器IA32_EFER LME位
        mov ecx, 0xc0000080
        rdmsr
        bts eax, 8
        wrmsr

        ; 开启分页
        mov eax, cr0
        bts eax, 31
        mov cr0, eax

        ; 打印IA-32e激活信息
        mov ebx, LDR_PHY_ADDR + ia_32e
        call put_string

        
        push word CORE_CODE64_SEL               ; 伪造远过程返回栈信息，压入64位代码段选择子
        mov eax, [CORE_PHY_ADDR + 0x04]         ; 内核入口点偏移量
        add eax, CORE_PHY_ADDR                  ; 内核入口点偏移量 + 内核的起始物理地址（此时线性地址和物理地址相同）= 内核入口点线性地址
        push eax                                ; 伪造远过程返回栈信息，压入内核入口点线性地址
        retf                                    ; 通过远返回的方式进入内核



put_string:                                     ; 过程：打印字符串
                                                ; 输入：ebx 字符串地址，0结尾
        push ebx
        push ecx

    .next_char:
        mov cl, [ebx]
        cmp cl, 0
        jz .exit                                ; 如果当前字符是0，结束过程
        
        call put_char                           ; 否则调用put_char打印该字符
        inc ebx                                 ; bx指向下一个字符
        jmp .next_char

    .exit:
        pop ecx
        pop ebx

        ret



put_char:                                       ; 过程：打印单个字符
                                                ; 输入：cl = 单个字符的ASCII码

        push eax
        push ebx
        push ecx
        push edx
        push esi
        push edi

        call get_cursor                         ; 读取光标位置到ax中

        cmp cl, 0x0d                            ; 判断要打印的字符是否回车\r
        jz .print_CR

        cmp cl, 0x0a                            ; 判断要打印的字符是否换行\n
        jz .print_LF
        
        jmp .put_other                          ; 打印其它字符

    ; 打印回车字符，使光标移至行首
    .print_CR:
        mov bl, 80
        div bl                                  ; 当前光标位置除以80得到行号在al中
        mul bl                                  ; 上一步计算得出的行号×80便是当前行行首，结果在ax中
        mov bx, ax
        jmp .setcur

    ; 以下打印换行符，将光标移至下一行的当前位置
    .print_LF:
        mov bx, ax                              ; 将ax中的结果临时保存至bx
        add bx, 80                              ; 将当前光标位置+80就是下一行当前位置
        jmp .roll_screen

    ; 以下打印其它字符
    .put_other:
        movzx ebx, ax                                   ; 0扩展传送ax到ebx
        shl bx, 1                                       ; 将光标位置左移1位，相当于×2，得到显存中的字符偏移位置
        mov [ebx + 0x000b8000], cl                      ; 将cl中的ASCII码写入显存
        mov byte [ebx + 0x000b8000 + 1], 00000111B      ; 显示属性
        
        ; 将光标位置还原并+1
        shr bx, 1
        inc bx
        
    ; 滚屏
    .roll_screen:
        cmp bx, 2000
        jl .setcur                                      ; 如果当前光标位置 < 2000，则不需要滚屏，直接设置光标位置

        ; 滚屏操作
        ; 将ds:si指向屏幕第1行开始位置，di指向屏幕第0行开始位置

        mov esi, 0x000b8000 + 0xa0
        mov edi, 0x000b8000
        mov ecx, 1920 / 2                               ; 传送1920 / 2个双字
        cld                                             ; 清方向标志，表示正向传送
        rep movsd

        ; 清除屏幕最底下一行
        mov ecx, 80
    .clear_last:
        mov byte [edi], ' '                             ; 使用空格填充
        mov byte [edi + 1], 01000000B                   ; 显示属性
        add edi, 2
        loop .clear_last

        sub ebx, 80                                     ; 滚屏后，重新将光标位置设置在最后一行的相应位置

    .setcur:
        call set_cursor

        pop edi
        pop esi
        pop edx
        pop ecx
        pop ebx
        pop eax

        ret


get_cursor:                                         ; 过程：获取光标位置
                                                    ; 返回：ax = 光标位置
        push dx

        ; 通过索引寄存器使数据寄存器指向光标寄存器高8位
        mov dx, 0x3d4
        mov al, 0x0e
        out dx, al

        ; 通过数据端口获取光标寄存器高8位
        mov dx, 0x3d5
        in al, dx
        mov ah, al

        ; 通过索引寄存器使数据寄存器指向光标寄存器低8位
        mov dx, 0x3d4
        mov al, 0x0f
        out dx, al

        ; 将光标寄存器低8位读入al中
        mov dx, 0x3d5
        in al, dx                                   ; 此时ax中有了完整的光标位置（高8位和低8位）

        pop dx
        ret


set_cursor:                                         ; 过程：设置光标位置
                                                    ; 输入：bx = 光标位置
        push eax
        push edx
        
        ; 通过索引寄存器将数据寄存器指向光标高8位
        mov dx, 0x3d4
        mov al, 0x0e
        out dx, al

        ; 通过数据寄存器写入光标高8位
        mov dx, 0x3d5
        mov al, bh
        out dx, al

        ; 通过索引寄存器将数据寄存器指向光标低8位
        mov dx, 0x3d4
        mov al, 0x0f
        out dx, al

        ; 通过数据寄存器写入光标高8位
        mov dx, 0x3d5
        mov al, bl
        out dx, al

        pop edx
        pop eax

        ret



read_hard_disk_0:                       ; 过程：读取硬盘一个扇区
                                        ; 入参：eax=逻辑扇区号
                                        ;       ds:ebx=目标缓冲区地址
                                        ; 返回：ebx=ebx+512
        push eax
        push ecx
        push edx

        push eax

        ; 设置读取扇区数=1
        mov dx, 0x01f2
        mov al, 0x01
        out dx, al

        pop eax
        
        ; 设置起始扇区号
        inc dx                          ; 0x01f3
        out dx, al                      ; LBA 0~7

        inc dx                          ; 0x01f4
        mov al, ah
        out dx, al                      ; LBA 8~15

        inc dx                          ; 0x01f5
        shr eax, 16
        out dx, al                      ; LBA 16~23

        inc dx                          ; 0x01f6
        mov al, ah
                                        ; 位5位7固定是1，位6=1代表LBA模式，位4=0代表主硬盘，低4位暂设为0
        or al, 1110_0000B               ; 此时al低4位保存了LBA28的最高4位
        out dx, al

        inc dx                          ; 0x01f7
        mov al, 0x20                    ;读指令
        out dx, al

        ; 判断硬盘是否可读
    .waits:
        in al, dx
        and al, 1000_1000B
        cmp al, 0000_1000B
        jnz .waits

        ; 硬盘准备好数据后，开始读入内存
        mov ecx, 256
        mov dx, 0x01f0
    .readw:
        in ax, dx
        mov [ebx], ax
        add ebx, 2
        loop .readw
        
        pop edx 
        pop ecx 
        pop eax

        ret


section tail
    loader_end: